4.4 匿名函数

匿名函数是指没有定义名字符号的函数。

除没有名字外,匿名函数和普通函数完全相同。最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

直接执行:

func main() { 
   func(s string) { 
       println(s) 
    }("hello,world!") 
}

赋值给变量:

func main() { 
   add:=func(x,y int)int{ 
       return x+y
    } 
  
   println(add(1,2)) 
}
 

作为参数:

func test(f func()) { 
   f() 
} 
  
func main() { 
   test(func() { 
       println("hello,world!") 
    }) 
}

作为返回值:

func test() func(int,int)int{ 
   return func(x,y int)int{ 
       return x+y
    } 
} 
  
func maidn() { 
   add:=test() 
   println(add(1,2)) 
}

将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。当然,编译器会为匿名函数生成一个“随机”符号名。

普通函数和匿名函数都可作为结构体字段,或经通道传递。

func testStruct() { 
   type calc struct{                // 定义结构体类型 
       mul func(x,y int)int         // 函数类型字段 
    } 
  
   x:=calc{ 
       mul:func(x,y int)int{ 
           return x*y
        }, 
    } 
  
   println(x.mul(2,3)) 
} 
  
func testChannel() { 
   c:=make(chan func(int,int)int,2) 
  
   c<-func(x,y int)int{ 
       return x+y
    } 
  
   println((<-c)(1,2)) 
}
 

不曾使用的匿名函数会被编译器当作错误。

func main() { 
   func(s string) {      // 错误:func literal evaluated but not used
       println(s) 
    }          // 此处并未调用 
}

除闭包因素外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。

相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。

闭包

闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。这种说明太学术范儿了,很难理解,我们先看一个例子。

func test(x int)func() { 
   return func() { 
       println(x) 
    } 
} 
  
func main() { 
   f:=test(123) 
   f() 
}

输出:

123

就这段代码而言,test返回的匿名函数会引用上下文环境变量x。当该函数在main中执行时,它依然可正确读取x的值,这种现象就称作闭包。

闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量值?修改一下代码再看。

package main
  
func test(x int)func() { 
   println(&x) 
  
   return func() { 
       println(&x,x) 
    } 
} 
  
func main() { 
   f:=test(0x100) 
   f() 
}

输出:

0xc82000a100
0xc+82000a100 256

通过输出指针,我们注意到闭包直接引用了原环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切

本质上返回的是一个funcval结构,可在runtime/runtime2.go中找到相关定义。

$go build-gcflags"-N-l"  # 禁用内联和代码优化 
  
  
$gdb test
  
(gdb)b 6         # 设置断点后,执行 
(gdb)b 13
(gdb)r
  
(gdb)info locals       # 进入test函数,获取环境变量x地址 
&x=0xc82000a130
  
(gdb)c                    # 继续执行,回到main函数 
  
(gdb)disas
Dump of assembler code for function main.main: 
  0x000000000000213f<+15>:   sub  rsp,0x18
  0x0000000000002143<+19>:   mov  QWORD PTR[rsp],0x100
  0x000000000000214b<+27>:   call 0x2040<main.test> 
  0x0000000000002150<+32>:   mov  rbx,QWORD PTR[rsp+0x8]     #test返回值 
  0x0000000000002155<+37>:   mov  QWORD PTR[rsp+0x10],rbx
=>0x000000000000215a<+42>:   mov  rbx,QWORD PTR[rsp+0x10] 
  0x000000000000215f<+47>:   mov  rdx,rbx                # 保存到rdx寄存器 
  0x0000000000002162<+50>:   mov  rbx,QWORD PTR[rdx] 
  0x0000000000002165<+53>:   call rbx
  0x0000000000002167<+55>:   add  rsp,0x18
  0x000000000000216b<+59>:   ret  
  
(gdb)x/2xg$rbx       # 包含匿名函数和环境变量地址 
0xc82000a140:  0x0000000000002180 0x000000c82000a130
  
(gdb)info symbol 0x0000000000002180
main.test.func1 in section.text
  
(gdb)s                    # 继续,进入匿名函数 
Breakpoint 1,main.test.func1
  
(gdb)disas
Dump of assembler code for function main.test.func1: 
  0x000000000000218f<+15>:   sub  rsp,0x18
  0x0000000000002193<+19>:   mov  rbx,QWORD PTR[rdx+0x8]      # 经rdx读取环境变量地址 
  0x0000000000002197<+23>:   mov  QWORD PTR[rsp+0x10],rbx
  0x000000000000219c<+28>:   mov  rbx,QWORD PTR[rsp+0x10] 
  0x00000000000021a1<+33>:   mov  QWORD PTR[rsp+0x8],rbx
  0x00000000000021a6<+38>:   call 0x23ac0<runtime.printlock> 
  0x00000000000021ab<+43>:   mov  rbx,QWORD PTR[rsp+0x8] 
  0x00000000000021b0<+48>:   mov  QWORD PTR[rsp],rbx
  0x00000000000021b4<+52>:   call 0x244a0<runtime.printpointer> 
  
(gdb)x/1xg$rdx+0x8
0xc82000a148:  0x000000c82000a130

正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性

func test() []func() { 
   var s[]func() 
  
   for i:=0;i<2;i++ { 
       s=append(s,func() {      // 将多个匿名函数添加到列表 
           println(&i,i) 
        }) 
    } 
  
   return s               // 返回匿名函数列表 
} 
  
func main() { 
   for_,f:=range test() {     // 迭代执行所有匿名函数 
       f() 
    } 
}

输出:

0xc82000a078 2
0xc82000a078 2

对这个输出结果不必惊讶。很简单,for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当main执行这些函数时,它们读取的是环境变量i最后一次循环时的值。不是2,还能是什么?

解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。

func test() []func() { 
   var s[]func() 
  
   for i:=0;i<2;i++ { 
       x:=i               //x每次循环都重新定义 
       s=append(s,func() { 
           println(&x,x) 
        }) 
    } 
  
   return s
}

输出:

0xc82006e000 0
0xc82006e008 1

多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。

func test(x int) (func(),func()) {   // 返回两个匿名函数 
   return func() {              
           println(x) 
           x+=10       // 修改环境变量 
        },func() {               
           println(x)          // 显示环境变量 
        } 
} 
  
func main() { 
   a,b:=test(100) 
   a() 
   b() 
}
 

输出:

100
110

闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用。